/**
 * @name reconnect-websocket
 * @author NimitzDEV
 * @license MIT
 * @desc 基于浏览器的 websocket 对象，封装一个具有自动重连功能的 websocket
 * @example 
 *  let rws = new ReconnectingWebSocket(url, protocols, options);
 */

interface rws {
    /**
     * 为 IE 9 - IE 11 生成兼容的事件
     * @param en 事件的名称
     * @param args 可选参数；事件可能会用到的对象
     */
    generateEvent(en: string, args?: Object): CustomEvent;

    /**
     * 发送信息
     * @param data 要发送的字符串
     */
    send(data: string): void;

    /**
     * 打开 WS 连接
     * @param reconnectAttempt 是否自动重连
     */
    open(reconnectAttempt: boolean): void;

    /**
     * 关闭连接
     * @param code 异常代码
     * @param reason 关闭原因
     */
    close(code: number, reason: string): void;

    /**
     * 刷新
     */
    refresh(): void;
}

interface rwsOptions {
    /** 当前实例是否需要输出调试信息 */
    debug?: boolean;

    /** 是否自动开启连接 */
    automaticOpen?: boolean;

    /** 自动重连间隔时间 */
    reconnectInterval?: number;

    /** 最大自动重连时间间隔 */
    maxReconnectInterval?: number;

    /** 自动重连时间间隔增加倍数 */
    reconnectDecay?: number;

    /** 超时间隔 */
    timeoutInterval?: number;

    /** 最高自动重连尝试次数 null 为无限次数 */
    maxReconnectAttempts?: number;

    /** 二进制格式*/
    binaryType?: string
}

export class ReconnectingWebSocket implements rws {
    private options: rwsOptions = {
        debug: false,
        automaticOpen: true,
        reconnectInterval: 1000,
        maxReconnectInterval: 30000,
        reconnectDecay: 1.5,
        timeoutInterval: 2000,
        maxReconnectAttempts: null,
        binaryType: 'blob'
    }
    private ws: WebSocket;
    private forcedClose: boolean = false;
    private timedOut: boolean = false;
    private eventTarget: HTMLDivElement = document.createElement('div');
    private reconnectAttempts: number = 0;
    private protocol: string = null;

    readyState: number = WebSocket.CONNECTING;
    url: string;
    protocols: string;

    onopen: Function = (e) => { }
    onclose: Function = (e) => { }
    onconnecting: Function = (e) => { }
    onmessage: Function = (e) => { }
    onerror: Function = (e) => { }

    constructor(url: string, protocols?: string, options?: rwsOptions) {
        if (!('WebSocket' in window)) return;
        // 导入自定义设置
        options = options || {};
        Object.keys(options).forEach(v => this.options[v] = options[v]);
        this.url = url;
        this.protocols = protocols;

        // 绑定事件
        this.eventTarget.addEventListener('open', e => this.onopen(e));
        this.eventTarget.addEventListener('close', e => this.onclose(e));
        this.eventTarget.addEventListener('connecting', e => this.onconnecting(e));
        this.eventTarget.addEventListener('message', e => this.onmessage(e));
        this.eventTarget.addEventListener('error', e => this.onerror(e));

        // 暴露 API
        this['addEventListener'] = this.eventTarget.addEventListener.bind(this.eventTarget);
        this['removeEventListener'] = this.eventTarget.removeEventListener.bind(this.eventTarget);
        this['dispatchEvent'] = this.eventTarget.dispatchEvent.bind(this.eventTarget);

        if (this.options.automaticOpen) this.open(false);
    }

    generateEvent(en: string, args?: Object) {
        let evt = document.createEvent('CustomEvent');
        evt.initCustomEvent(en, false, false, args);
        return evt;
    }

    open = (reconnectAttempt: boolean) => {
        this.ws = new WebSocket(this.url, this.protocols || []);
        this.ws.binaryType = this.options.binaryType;

        // 检查重连状况
        if (reconnectAttempt) {
            if (this.options.maxReconnectAttempts && this.reconnectAttempts > this.options.maxReconnectAttempts) return;
        } else {
            this.eventTarget.dispatchEvent(this.generateEvent('connecting'));
            this.reconnectAttempts = 0;
        }

        if (this.options.debug) console.log('rws', 'attemp-reconnect', this.url);

        let localWs = this.ws;
        let timeout = setTimeout(() => {
            if (this.options.debug) console.log('rws', 'connection-timeout', this.url);
            this.timedOut = true;
            localWs.close();
            this.timedOut = false;
        }, this.options.timeoutInterval);

        this.ws.onopen = (e) => {
            clearInterval(timeout);
            if (this.options.debug) console.log('rws', 'onopen', this.url);
            this.protocol = this.ws.protocol;
            this.readyState = WebSocket.OPEN;
            this.reconnectAttempts = 0;

            let ev = this.generateEvent('open');
            ev['isReconnect'] = reconnectAttempt;
            reconnectAttempt = false;
            this.eventTarget.dispatchEvent(ev);
        }

        this.ws.onclose = (e) => {
            clearTimeout(timeout);
            this.ws = null;
            if (this.forcedClose) {
                this.readyState = WebSocket.CLOSED;
                this.eventTarget.dispatchEvent(this.generateEvent('close'));
                return;
            }
            this.readyState = WebSocket.CONNECTING;
            let ev = this.generateEvent('connecting');
            ev['code'] = e.code;
            ev['reason'] = e.reason;
            ev['wasClean'] = e.wasClean;

            this.eventTarget.dispatchEvent(ev);

            if (!reconnectAttempt && this.timedOut) {
                if (this.options.debug) console.log('rws', 'onclose', this.url);
                this.eventTarget.dispatchEvent(this.generateEvent('close'));
            }

            let timeout2 = this.options.reconnectInterval * Math.pow(this.options.reconnectDecay, this.reconnectAttempts);
            setTimeout(() => {
                this.reconnectAttempts++;
                this.open(true);
            }, timeout2 > this.options.maxReconnectInterval ? this.options.maxReconnectInterval : timeout2);
        }

        this.ws.onmessage = (e) => {
            if (this.options.debug) console.log('rws', 'onmessage', this.url);
            let ev = this.generateEvent('message');
            ev['data'] = e.data;
            this.eventTarget.dispatchEvent(ev);
        }

        this.ws.onerror = (e) => {
            if (this.options.debug) console.log('rws', 'onerror', this.url, e);
            this.eventTarget.dispatchEvent(this.generateEvent('error'));
        }
    }

    send = (data: string) => {
        if (this.ws) {
            if (this.options.debug) console.log('rws', 'send', this.url, data);
            return this.ws.send(data);
        }
    }

    close = (code: number, reason: string) => {
        code = code || 1000;
        this.forcedClose = true;
        if (this.ws) this.ws.close(code, reason);
    }

    refresh = () => {
        if (this.ws) this.ws.close();
    }

}